package cn.com.scooper.config;

import java.io.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 配置文件工具类.<br />
 * 支持功能：
 * <ul>
 * 	<li>配置文件搬移</li>
 * 	<li>配置文件加载，支持加载多个配置文件，多个文件允许出现相同的配置项</li>
 * 	<li>配置项值引用和替换</li>
 * 	<li>配置项值加载</li>
 * </ul>
 * 
 * @author LuLihong
 *
 */
public final class ConfigKit {
	private static final String DIR_ROOT_LINUX = "/icooper";

	private static final String DIR_CONFIG_HOME = "config";
	/**
	 * 文件默认编码格式
	 */
	protected static final String DEFAULT_ENCODING = "UTF-8";
	/**
	 * 程序配置目录
	 */
	private static File appConfigHome = null;
	/**
	 * 默认使用的Prop，指向第一个加载的配置文件
	 */
	private static ConfigItem configItem = null;
	/**
	 * 配置文件Map
	 */
	private static Map<String, ConfigItem> configMap = new ConcurrentHashMap<String, ConfigItem>();

	/**
	 * 初始化
	 * @param appName	程序名称
	 */
	public static void init(String appName) {
		init(appName, getConfigHome());
	}
	
	/**
	 * 初始化，最终配置文件存放目录: configHome/appName
	 * @param appName		程序名称
	 * @param configHome	配置文件存放根目录
	 */
	public static void init(String appName, String configHome) {
		if (isEmpty(appName)) {
			throw new IllegalArgumentException("appName can't be null or empty");
		}
		if (configHome == null) {
			throw new IllegalArgumentException("configHome can't be null");
		}
		
		appConfigHome = new File(configHome, appName);
		if (!appConfigHome.exists()) {
			appConfigHome.mkdirs();
		}
		System.out.println("App config dir:" + appConfigHome.getPath());
	}
	
	/**
	 * 加载配置文件
	 * @param filename
	 */
	public static void loadConfig(String filename) {
		loadConfig(filename, filename);
	}
	
	/**
	 * 加载配置文件，存储在内存Map中
	 * @param filename	文件名
	 * @param alias		配置文件别名，调用use时使用该值获取Prop对象
	 */
	public static void loadConfig(String filename, String alias) {
		loadConfig(filename, alias, true, DEFAULT_ENCODING);
	}
	
	/**
	 * 加载配置文件
	 * @param filename	文件名
	 * @param alias		配置文件别名，调用use时使用该值获取Prop对象
	 * @param isCopyConfig	是否复制配置文件到公开的目录，比如linux上复制到/icooper/config目录下。
	 */
	public static void loadConfig(String filename, String alias, boolean isCopyConfig) {
		loadConfig(filename, alias, isCopyConfig, DEFAULT_ENCODING);
	}
	
	/**
	 * 加载配置文件
	 * @param filename	文件名
	 * @param alias		配置文件别名，调用use时使用该值获取Prop对象
	 * @param isCopyConfig	是否复制配置文件到公开的目录
	 * @param encoding		读取文件编码
	 */
	public static void loadConfig(String filename, String alias, boolean isCopyConfig, String encoding) {
		if (isCopyConfig) {
			loadConfig(filename, alias, encoding);
		} else {
			loadConfigInProject(filename, alias, encoding);
		}
	}

	/**
	 * 直接加载配置文件
	 * @param configFile	配置文件
	 * @param alias			别名
	 */
	public static void loadConfig(File configFile, String alias) {
		loadConfig(configFile, alias, DEFAULT_ENCODING);
	}

	/**
	 * 直接加载配置文件
	 * @param configFile	配置文件
	 * @param alias			别名
	 * @param encoding		文件编码
	 */
	public static void loadConfig(File configFile, String alias, String encoding) {
		ConfigItem appConfig = new ConfigItem(configFile, encoding);
		configMap.put(alias, appConfig);

		if (ConfigKit.configItem == null) {
			ConfigKit.configItem = appConfig;
		}
	}
	
	/**
	 * 加载配置文件，将配置文件搬移到公开目录
	 * @param filename
	 * @param alias		配置文件别名
	 * @param encoding	编码
	 */
	private static void loadConfig(String filename, String alias, String encoding) {
		if (isEmpty(filename)) {
			throw new IllegalArgumentException("filename can't be null or empty");
		}
		if (isEmpty(alias)) {
			throw new IllegalArgumentException("alias can't be null or empty");
		}
		File configFile = null;
		try {
			configFile = copyConfigFile(filename);
		} catch (IOException e){
			System.err.println(String.format("Copy config file[%s] to dir[%s] error: %s", filename, appConfigHome, e.getMessage()));
		}
		
		if (!configMap.containsKey(alias)) {
			//系统配置文件（搬移过去的）
			ConfigItem systemConfig = new ConfigItem(configFile, encoding);
			//项目配置文件（跟随项目发布的）
			ConfigItem appConfig = new ConfigItem(filename, encoding);
			Properties properties = appConfig.getProperties();
			//发布的项目配置文件可能有所增加
			for (Object key: properties.keySet()){
				if (!systemConfig.getProperties().containsKey(key)) {
					Object value = properties.get(key);
					systemConfig.getProperties().put(key, value);
					System.out.println(String.format("File[%s] add new config[%s=%s]", filename, key, String.valueOf(value)));
				}
			}
			configMap.put(alias, systemConfig);
			//分配第一个配置文件
			if (ConfigKit.configItem == null) {
				ConfigKit.configItem = systemConfig;
			}
		}
	}
	
	/**
	 * 在项目中加载配置文件，配置文件不搬移。
	 * @param filename
	 * @param alias
	 * @param encoding
	 */
	private static void loadConfigInProject(String filename, String alias, String encoding) {
		ConfigItem appConfig = new ConfigItem(filename, encoding);
		configMap.put(alias, appConfig);
		
		if (ConfigKit.configItem == null) {
			ConfigKit.configItem = appConfig;
		}
	}
	
	/**
	 * 获取alias所对应的ConfigItem
	 * @param alias		配置文件别名
	 * @return 返回Prop对象
	 */
	public static ConfigItem use(String alias) {
		if (!configMap.containsKey(alias)) {
			throw new RuntimeException(String.format("Can't find config by alias[%s], plz call ConfigKit.loadProp(%s) before", alias, alias));
		}
		return configMap.get(alias);
	}
	
	/**
	 * 将配置文件复制到程序配置目录${appConfigHome}
	 * @param filename
	 * @throws IOException
	 */
	private static File copyConfigFile(String filename) throws IOException {
		String newFilename = filename;
		int index = filename.lastIndexOf(File.separator);
		if (index >= 0) {
			newFilename = filename.substring(index + File.separator.length());
		}
		index = filename.lastIndexOf("/");
		if (index >= 0) {
			newFilename = filename.substring(index + 1);
		}
		
		return copyConfigFile(appConfigHome, newFilename, filename);
	}
	
	/**
	 * 复制文件
	 * @param dir
	 * @param newfilename
	 * @param srcFile
	 * @return
	 * @throws IOException
	 */
	private static File copyConfigFile(File dir, String newFilename, String srcFile) throws IOException {
		File copyFile = new File(dir, newFilename);
		if (copyFile.exists()) {
			return copyFile;
		}
		
		BufferedReader reader = null;
		BufferedWriter writer = null;
		String lineInfo = null;
		try {
			InputStream inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(srcFile);
			reader = new BufferedReader(new InputStreamReader(inputStream));
			writer = new BufferedWriter(new FileWriter(copyFile));
			while ((lineInfo = reader.readLine()) != null) {
				writer.write(lineInfo);
				writer.newLine();
			}
		} finally {
			if (writer != null) {
				writer.close();
			}
			if (reader != null) {
				reader.close();
			}
		}
		System.out.println(String.format("Copy config file[%s] to dir[%s] succ", srcFile, appConfigHome));
		
		return copyFile;
	}
	
	/**
	 * 获取配置文件存储目录
	 * <ul>
	 * 	<li>windows: 当前用户目录下的config目录 </li>
	 * 	<li>linux: /icooper/config目录</li>
	 * </ul>
	 * @return 返回配置文件存储根目录
	 */
	public static String getConfigHome() {
		boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
		String rootDir = isWindows ? System.getProperty("user.home") : DIR_ROOT_LINUX;
		
		return rootDir + File.separator + DIR_CONFIG_HOME ;
	}
	
	private static boolean isEmpty(String str) {
		return str == null || str.trim().isEmpty();
	}
	
	/**
	 * 在所有配置文件中查找配置项值，同时搜索该值中模式为{key}的部分，
	 * 将key提取出来，从所有配置文件中查询该key对应的值来替换{key}，支持多个{key}。
	 <br/>配置项如下：
	 <ul>
	 	<li>app.test.ip=192.168.103.172</li>
	 	<li>app.test.port=3306</li>
	 	<li>app.test.db=SSMS_DB</li>
	 	<li>app.test.url=jdbc:mysql://{app.test.ip}:{app.test.port}/{app.test.db}</li>
	 </ul>
	 获取的结果为：jdbc:mysql://192.168.103.172:3306/SSMS_DB
	 *
	 * @param configKey		配置项
	 * @return 替换后的配置项值
	 */
	public static String findReplace(String configKey) {
		String configVal = find(configKey, "").trim();
		if (isEmpty(configVal)) {
			return "";
		}
		
		Set<String> replaceKeys = parseReplaceKey(configVal);
		if (replaceKeys.size() == 0) {
			return configVal;
		}
		
		for (String replaceKey: replaceKeys) {
			String replaceValue = find(replaceKey, "").trim();
			replaceKey = "{" + replaceKey + "}";
			if (configVal.contains(replaceKey)) {
				configVal = configVal.replace(replaceKey, replaceValue);
			}
		}
		
		return configVal;
	}

	private static final Pattern PATTERN_KEY = Pattern.compile("\\{[^\\}]+\\}");
	/**
	 * 解析配置值中{key}中的所有key的集合
	 * @param configVal
	 * @return key的集合
	 */
	public static Set<String> parseReplaceKey(String configVal) {
		Matcher matcher = PATTERN_KEY.matcher(configVal);
		Set<String> replaceKeys = new HashSet<String>();
		while (matcher.find()) {
			String key = matcher.group().replaceAll("[\\{\\}]", "");
			if (!isEmpty(key)) {
				replaceKeys.add(key);
			}
		}
		
		return replaceKeys;
	}
	
	/**
	 * 获取默认配置文件中获取配置项值，获取方式见findReplace，不同的是查询key所对应的值只在默认文件中。
	 * @param configKey		配置项
	 * @return 替换后的配置项值
	 */
	public static String getReplace(String configKey) {
		return getConfigItem().getReplace(configKey);
	}
	
	/**
	 * 获取指定配置文件中获取配置项值，获取方式见findReplace，不同的是查询key所对应的值只在指定文件中。
	 * @param configKey		配置项
	 * @param alias			配置文件别名
	 * @return 替换后的配置项值
	 */
	public static String getReplace(String configKey, String alias) {
		return use(alias).getReplace(configKey);
	}
	
	/**
	 * 移除配置文件别名所对应的Prop
	 * @param alias		配置文件别名
	 * @return 返回移除后的Prop对象
	 */
	public static ConfigItem remove(String alias) {
		ConfigItem previous = configMap.remove(alias);
		if (configItem == previous) {
			configItem = null;
		}
		return previous;
	}
	
	public static void clear() {
		configItem = null;
		configMap.clear();
	}
	
	public static ConfigItem getConfigItem() {
		if (configItem == null) {
			throw new IllegalStateException("Load propties file by invoking ConfigItem.use(String filename) method first.");
		}
		return configItem;
	}
	
	public static ConfigItem getConfigItem(String filename) {
		return configMap.get(filename);
	}
	
	public static String get(String key) {
		return getConfigItem().get(key);
	}
	
	public static String get(String key, String defaultValue) {
		return getConfigItem().get(key, defaultValue);
	}
	
	/**
	 * 在所有配置文件中查找该值， 如果未找到，返回默认值
	 * @param key
	 * @param defaultValue  
	 * @return 返回配置项值
	 */
	public static String find(String key, String defaultValue) {
		for (ConfigItem item: configMap.values()) {
			if (item.containsKey(key)) {
				return item.get(key, defaultValue);
			}
		}
		return defaultValue;
	}
	
	public static Integer getInt(String key) {
		return getConfigItem().getInt(key);
	}
	
	public static Integer getInt(String key, Integer defaultValue) {
		return getConfigItem().getInt(key, defaultValue);
	}
	
	/**
	 * 在所有配置文件中查找该值， 如果未找到，返回默认值
	 * @param key
	 * @param defaultValue
	 * @return 返回配置项值
	 */
	public static Integer findInt(String key, Integer defaultValue) {
		for (ConfigItem item: configMap.values()) {
			if (item.containsKey(key)) {
				return item.getInt(key, defaultValue);
			}
		}
		return defaultValue;
	}
	
	public static Long getLong(String key) {
		return getConfigItem().getLong(key);
	}
	
	public static Long getLong(String key, Long defaultValue) {
		return getConfigItem().getLong(key, defaultValue);
	}
	
	/**
	 * 在所有配置文件中查找该值，如果未找到，返回默认值
	 * @param key
	 * @param defaultValue
	 * @return 返回配置项值
	 */
	public static Long findLong(String key, Long defaultValue) {
		for (ConfigItem item: configMap.values()) {
			if (item.containsKey(key)) {
				return item.getLong(key, defaultValue);
			}
		}
		return defaultValue;
	}
	
	public static Boolean getBoolean(String key) {
		return getConfigItem().getBoolean(key);
	}
	
	public static Boolean getBoolean(String key, Boolean defaultValue) {
		return getConfigItem().getBoolean(key, defaultValue);
	}
	
	/**
	 * 在所有配置文件中查找该值，如果未找到，返回默认值
	 * @param key
	 * @param defaultValue
	 * @return 返回配置项值
	 */
	public static Boolean findBoolean(String key, Boolean defaultValue) {
		for (ConfigItem item: configMap.values()) {
			if (item.containsKey(key)) {
				return item.getBoolean(key, defaultValue);
			}
		}
		return defaultValue;
	}
	
	/**
	 * 根据前缀在所有配置文件中加载配置项
	 * @param prefix
	 * @return
	 */
	public static Map<String, String> getByPrefix(String prefix) {
		Map<String, String> map = new HashMap<String, String>(5);
		
		if (prefix == null || prefix.isEmpty()) {
			return map;
		}
		
		for (ConfigItem item: configMap.values()) {
			map.putAll(item.getByPrefix(prefix));
		}
		
		return map;
	}
	
	public static boolean containsKey(String key) {
		return getConfigItem().containsKey(key);
	}
}

